import os
import sys
import time
from PySide6.QtGui import QIcon,QTextCursor
from PySide6.QtCore import (Slot,QObject,Signal,QRunnable,QThreadPool,QSettings)
from PySide6.QtWidgets import (
    QLineEdit,
    QWidget, 
    QMainWindow, 
    QPushButton,
    QFileDialog,
    QMessageBox,
    QPlainTextEdit,
    QDialog,
    QVBoxLayout)
from PySide6.QtUiTools import QUiLoader
from MyConfig import MyConfig
from MyLogger import MyLogger,EmittingStream
import pandas as pd
import ntpath
import warnings
from  MainConsolidator import MainConsolidator
from OraWorker import OraWorker
from TodoWindow import TodoWindow
from GuidsWindow import GuidsWindow
import pandas_xlwt # не убирать!!!
import urllib.request 
from http import client
from GuidsSync import GuidsSync
import traceback


class Semaphore(QObject):
    """ Класс-семафор для отслеживания состояния потока """
    started = Signal()
    finished = Signal()

class Worker(QRunnable):
    """"
    Worker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    :param callback: The function callback to run on this worker thread. Supplied args and
                     kwargs will be passed through to the runner.
    :type callback: function
    :param args: Arguments to pass to the callback function
    :param kwargs: Keywords to pass to the callback function

    """

    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.semaphore = Semaphore()

    @Slot()  # QtCore.Slot
    def run(self):
        """" Initialise the runner function with passed args, kwargs. """
        self.semaphore.started.emit()
        self.fn(*self.args, **self.kwargs)
        self.semaphore.finished.emit()

class ConsWindow(object):


    def __init__(self,metadata:dict=None,update_flag=False,update_url:str=None) -> None:
        try:
            self.version:int = 0
            self.busy = False
            try:
                self.cur_path = sys._MEIPASS
            except:
                self.cur_path = os.path.dirname(os.path.realpath(__file__))
                
            windowIcon = QIcon(f"{self.cur_path}\\consolidation.ico")
            self.output_dir = f"{self.cur_path}\\output\\"
            if not os.path.exists(self.output_dir): os.mkdir(self.output_dir)
            self.threadpool = QThreadPool()
            print(f"Multithreading with maximum {self.threadpool.maxThreadCount()} threads")
            loader = QUiLoader()
            es = EmittingStream()
            self.selected_files = []
            self.settings = MyConfig(f"{self.cur_path}\\config.json")
            logFile:str = self.settings.get("LogFile",defaultValue="default.log")
            consConfig:str = self.settings.get("ConsolidationConfig",defaultValue=f"{self.cur_path}\\consolidation.json")
            self.consCfg = MyConfig(consConfig)
            self.todoList:list= self.consCfg.get("todo",None)
            guidsDir:str = self.settings.get("GuidsDir_",defaultValue=f"{self.cur_path}\\guids") #bug fix GuidsDir -> GuidsDir_
            
            if not os.path.isdir(guidsDir): guidsDir=f"{self.cur_path}\\guids"
            self.guids_path = guidsDir            
            self.mlog= MyLogger(__name__, log_file=logFile, outStream=es)

            # init components
            self.MainWindow:QMainWindow = loader.load(f"{self.cur_path}\\ConsWindow.ui", None)
            self.MainWindow.setWindowIcon(windowIcon)
            if metadata is not None:
                prodName = metadata.get("FileDescription","module")
                prodVersion = metadata.get("Version","0.0.0.0")
                self.MainWindow.setWindowTitle(f"{prodName} ver.{prodVersion}")
            self.consoleBox:QPlainTextEdit  = self.MainWindow.consoleBox
            self.openFilesButton:QPushButton = self.MainWindow.openFilesButton
            self.consButton:QPushButton = self.MainWindow.consButton
            self.filesInput:QLineEdit = self.MainWindow.filesInput
            self.todoButton:QPushButton = self.MainWindow.todoButton
            self.guidsButton:QPushButton = self.MainWindow.guidsButton            

            # init slot connections
            es.textSignal.connect(self.write_to_console)
            self.openFilesButton.clicked.connect(self.select_input_files)
            self.consButton.clicked.connect(self.start_consolidate)
            self.todoButton.clicked.connect(self.show_todo_list)
            self.guidsButton.clicked.connect(self.show_guids_window)
            self.MainWindow.destroyed.connect(self.app_destroy)

            if update_flag:
                self.mlog.info("Start update procedure")
                self.openFilesButton.setEnabled(False)
                self.todoButton.setEnabled(False)
                self.consButton.setEnabled(False)
                self.guidsButton.setEnabled(False)
                self.start_download_update(update_url)
            else:
                # init MainConsolidator
                self.consolidator = MainConsolidator(logFile,logStream=es)
                self.consolidator.load_guids_x(guidsDir)
                self.consolidator.columns = self.consCfg.get("columns",[])                
                # init 2 level handlers
                self.handlers={}
                self.hdict={}
                hlist = self.consCfg.get("handlers",None)
                for item in hlist:
                    module_path = f"{self.cur_path}\\handlers2\\{item['module']}.py"
                    with open(module_path,encoding="utf-8",mode="r") as f:
                        module_src = f.read()
                        self.handlers[item["key"]]=compile(module_src,"","exec")
                        self.hdict[item["key"]]=item["module"]
                        self.mlog.info(f"{item['key']} : {item['module']}")
                self.mlog.info("2 level handlers loaded")
                self.checkFile = self.settings.get("CheckFile","")

                # oracle connect
                user = self.settings.get("OracleConfig",defaultValue=None)["user"]
                passw = self.settings.get("OracleConfig",defaultValue=None)["pass"]
                dsn = self.settings.get("OracleConfig",defaultValue=None)["dsn"]
                cl = self.settings.get("OracleConfig",defaultValue=None)["client"]
                self.ora = OraWorker(user,passw,dsn,cl)
                ora_inf= self.ora.ora_version()
                self.mlog.info(f"Соединение с oracle:\n {ora_inf}")
                self.mlog.info(f"pid: {os.getpid()}")
        except:
            self.mlog.error("ConsWindow init error",exc_info=True)

    def app_destroy(self):
        try:
            self.ora.close()
            self.mlog.info("oracle disconnected")
        except:
            self.mlog.error("app destroy",exc_info=True)

    def app_error(self,mess:str):
        """ Сообщение об ошибке """
        QMessageBox.critical(self.MainWindow,"Ошибка",mess,QMessageBox.StandardButton.Yes,QMessageBox.StandardButton.Yes)
        self.mlog.error(mess)

    def write_to_console(self,mess:str):
        """Вывод в консоль приложения

        Args:
            mess (str): Текст сообщения\n
        """
        try:
            self.consoleBox.appendHtml(mess) 
        except Exception as exp:
            self.app_error(f"write_to_console error: {exp}")

    def write_char_to_console(self,char:str):
        self.consoleBox.moveCursor(QTextCursor.MoveOperation.PreviousCharacter,QTextCursor.MoveMode.KeepAnchor)
        self.consoleBox.insertPlainText(char)
        # self.consoleBox.setTextCursor(prev_cursor)
        

    def show_todo_list(self):
        try:
            df = pd.DataFrame(self.consCfg.get("handlers",None))
            todoWindow = TodoWindow(df)
            if todoWindow.show()==1:
                self.consCfg.set("handlers",todoWindow.data.to_dict("records"))
                self.consCfg.sync()
        except Exception as exp:
            self.app_error(f"show_todo_list error: {exp}")

    def show_guids_window(self):
        """ Показать окно справочников """
        try:
            # gs:GuidsSync=None
            # try:
            #     remoteHost=self.settings.get("RemoteGuidsHost",None)
            #     remoteUser=self.settings.get("RemoteUser",None)
            #     remotePass=self.settings.get("RemotePassword",None)
            #     remoteDir=self.settings.get("RemoteGuidsDir",None)
            #     gs = GuidsSync(remoteHost,remoteUser,remotePass)
            #     gs.set_remote_dir(remoteDir)
            #     gs.set_local_dir(self.guids_path)
            # except Exception as e1: self.mlog.warning("Не удалось инициализировать модуль синхронизации справочников",exc_info=True)
            # guidsWindow = GuidsWindow(self.guids_path,syncModule=gs)
            guidsWindow = GuidsWindow()
            guidsWindow.show()
            self.mlog.info(f"Справочники изменены: {guidsWindow.guids_changed}")
            # issue fix http://vsys01775:8282/flea/pyexcelcons3/-/issues/5
            if guidsWindow.guids_changed: self.consolidator.load_guids_x(self.guids_path)
        except Exception as exp:
            self.app_error(f"show_guids_window error: {exp}")
            stacktrace = traceback.format_exc()
            self.app_error(stacktrace)

    def files_loaded(self):
        self.mlog.info("Файлы загружены")
        self.consButton.setDisabled(False)

    def select_input_files(self):
        """ Выбор файлов с шаблонами """
        try:
            fdialog=QFileDialog(parent=self.MainWindow,filter="MS Access 97 (*.mdb);; Excel (*.xlsx)")
            fdialog.setFileMode(QFileDialog.FileMode.ExistingFiles)
            if fdialog.exec()==1:
                self.selected_files = fdialog.selectedFiles()
                files_str = ", ".join([f"[{ntpath.basename(f)}]" for f in self.selected_files])
                self.filesInput.setText(files_str)
                self.mlog.info(f"Входные файлы: {files_str}")
                self.consButton.setDisabled(True)
                worker = Worker(self.start_load_input_files)
                worker.semaphore.finished.connect(self.files_loaded)
                self.threadpool.start(worker)
        except Exception as exp:
            self.app_error(f"select_input_files error: {exp}")

    def start_load_input_files(self):
        try:
            self.consolidator.load_input_data(self.selected_files)
            self.consolidator.dump_input_data(f"{self.output_dir}input_data.xlsx")
        except Exception as exp:
            self.mlog.error("start_load_input_files error",exc_info=True)


    def start_consolidate(self):
        try:
            worker = Worker(self.consolidate_items)
            worker.semaphore.finished.connect(lambda: self.mlog.info("Все готово"))
            self.threadpool.start(worker)
        except Exception as exp:
            self.mlog.error("start_consolidate error",exc_info=True)


    def handler_cc(self,rec:pd.Series)->pd.Series:
        try:
            result:pd.Series={"f1":0}
            module_name=f"__{self.hdict[rec['Производство2']]}__"
            namespace={"consolidator":self.consolidator , "record":rec, "result":result,"__name__":module_name}
            # self.mlog.info(module_name)
            exec(self.handlers[rec["Производство2"]],namespace)
            result = namespace["result"]
            self.mlog.info(f"{result['ID в SAP ERP VMZ']} -> {result['Продукт УП']}")
            return result
        except Exception as exp: 
            self.mlog.error("handler2 error",exc_info=True)
            rec["error"]=f"handler2_cc error: {exp}"
            return rec

    def check_handler(self,rec:pd.Series)->pd.Series:
        # rec["exists"] = self.ora.product_exists(str(rec["Продукт УП"]))
        # rec["ok"]=rec["Продукт УП"]==rec["Название продукта УП"]
        check = self.ora.product_check(str(rec["Продукт УП"]),str(rec["ID в SAP ERP VMZ"]))
        # rec["exists"] = ora.product_exists(str(rec["Продукт УП"]))
        # rec["ok"]=rec["Продукт УП"]==rec["Название продукта УП"]
        rec["ID в SAP ERP VMZ"]=str(rec["ID в SAP ERP VMZ"])
        rec["Есть в УП"] = check[1]
        rec["Уже укрупнено"] = check[2]
        rec["Уже укрупнено в"] = check[0]
        self.mlog.info(f"{rec['ID в SAP ERP VMZ']} : {rec['Продукт УП']} -> {check}")
        return rec
    
    def consolidate_items(self):
        try:
            if self.consolidator.input_data.empty: raise Exception("Отсутствуют входные данные")
            final_df = self.consolidator.input_data.apply(self.handler_cc,axis=1)
            final_df[self.consolidator.columns].to_excel(f"{self.output_dir}final_result.xlsx",index=False)
            self.mlog.info("Финальный результат сохранен на диск")

            ############# проверка #######################################
            df3:pd.DataFrame = final_df[["ID в SAP ERP VMZ","Полное наименование материала","Вид материала в SAP ERP","Продукт УП","error"]]
                        
            df_checked:pd.DataFrame = df3.apply(self.check_handler,axis=1)
            
            # outfile=f"{self.output_dir}\\final_checked.xls"
            # df_checked.to_excel(outfile,index=False,engine="xlwt")
            ### issue http://vsys01775:8282/flea/pyexcelcons3/-/issues/7 
            
            outfile=f"{self.output_dir}\\final_checked.xlsx"
            df_checked.to_excel(outfile,index=False,engine="openpyxl")            
            
            self.mlog.info("checked")
            os.system(f"start EXCEL.exe {outfile}")
            
            ##############################################################
        except Exception as exp:
            self.mlog.error("consolidate_items error",exc_info=True)   
            
    def start_download_update(self,update_url:str):
        try:
            worker = Worker(self.download_update,update_url)
            worker.semaphore.finished.connect(self.update_downloaded)
            self.threadpool.start(worker)
        except: raise
        
    def download_update(self,update_url:str):
        self.mlog.info(f"start download {update_url} \n")
        download_dir = f"{self.cur_path}\\update\\" if "_internal" not in self.cur_path else f"{self.cur_path}\\..\\..\\update\\"
        if not os.path.isdir(download_dir): os.makedirs(download_dir)
        mess=urllib.request.urlretrieve(update_url,f"{download_dir}update.zip")[0]
        self.mlog.info(mess)
        if "_internal" in self.cur_path:
            with open(f"{download_dir}update.bat",mode="w",encoding="windows-1251") as f:
                f.write(f'chcp 1251\ntaskkill /PID {os.getpid()}\npowershell -command "Expand-Archive -Force '+f"'{download_dir}update.zip' '{download_dir}..'"+'"\nexit')
                f.flush()
                f.close()
                os.system(f"start {download_dir}update.bat")
            
    def update_downloaded(self):
        self.openFilesButton.setEnabled(True)
        self.todoButton.setEnabled(True)
        self.consButton.setEnabled(True)
        self.guidsButton.setEnabled(True)
        self.mlog.info("update downloaded") 
        self.busy=False